import { GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerability_state_dropdown.vue';
import { VULNERABILITY_STATE_OBJECTS, DISMISSAL_REASONS } from 'ee/vulnerabilities/constants';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { dismissalDescriptions } from './mock_data';

const { dismissed, ...VULNERABILITY_STATE_OBJECTS_WITHOUT_DISMISSED } = VULNERABILITY_STATE_OBJECTS;
const states = Object.values(VULNERABILITY_STATE_OBJECTS).map((stateObject) => stateObject.state);
const statesWithoutDismissed = Object.values(VULNERABILITY_STATE_OBJECTS_WITHOUT_DISMISSED).map(
  (stateObject) => stateObject.state,
);
const dismissalReasons = Object.keys(DISMISSAL_REASONS);

describe('Vulnerability state dropdown component', () => {
  let wrapper;
  let hideDropdownMock;

  const createWrapper = ({
    initialState = statesWithoutDismissed[0],
    initialDismissalReason,
    disabled = false,
  } = {}) => {
    hideDropdownMock = jest.fn();

    const GlDropdownStub = stubComponent(GlDropdown, {
      template: RENDER_ALL_SLOTS_TEMPLATE,
      methods: {
        hide: hideDropdownMock,
      },
    });

    wrapper = shallowMountExtended(VulnerabilityStateDropdown, {
      propsData: { initialState, initialDismissalReason, disabled },
      stubs: { GlDropdown: GlDropdownStub },
      provide: {
        dismissalDescriptions,
      },
    });
  };

  const isSelected = (item) => item.find('.selected-icon').exists();
  const isDisabled = (item) => item.attributes('disabled') === 'true';
  const firstUnselectedItem = () => wrapper.find('.dropdown-item:not(.selected)');
  const selectedItem = () => wrapper.find('.dropdown-item.selected');
  const saveButton = () => wrapper.findComponent({ ref: 'save-button' });
  const cancelButton = () => wrapper.findComponent({ ref: 'cancel-button' });
  const innerDropdown = () => wrapper.findComponent(GlDropdown);
  const dropdownItemFor = (state) => wrapper.findByTestId(state);
  const dropdownItems = () => wrapper.findAll('.dropdown-item');
  const reasonDropdown = () => wrapper.findByTestId('dismissal-reason-dropdown');
  const reasonDropdownItems = () => reasonDropdown().findAll('.dropdown-item');
  const selectedReasonItem = () => reasonDropdown().find('.dropdown-item.selected');
  const helpPopoverFor = (reason) => wrapper.findByTestId(reason).findComponent(HelpPopover);
  const firstUnselectedReasonItem = () => reasonDropdown().find('.dropdown-item:not(.selected)');

  const mouseOverDismissed = async () => {
    await dropdownItemFor(dismissed.state).trigger('mouseover');
  };

  describe('tests that need to manually create the wrapper', () => {
    it.each(states)(
      'selects "%s" state when dropdown is created with that initial state',
      (state) => {
        createWrapper({ initialState: state });

        expect(isSelected(dropdownItemFor(state))).toBe(true);
      },
    );

    it.each(dismissalReasons)(
      'selects "%s" reason and "dismissed" state when dropdown is created with dismissed state and that reason',
      async (reason) => {
        createWrapper({ initialState: dismissed.state, initialDismissalReason: reason });

        expect(isSelected(dropdownItemFor(dismissed.state))).toBe(true);
        expect(selectedItem().text()).toMatch(DISMISSAL_REASONS[reason]);

        await mouseOverDismissed();

        expect(isSelected(dropdownItemFor(reason))).toBe(true);
      },
    );

    it('selects no state when dropdown is created with an unknown initial state', () => {
      createWrapper({ initialState: 'some unknown state' });

      dropdownItems().wrappers.forEach((dropdownItem) => {
        expect(isSelected(dropdownItem)).toBe(false);
      });
    });

    it('selects "dismissed" state and no reason when dropdown is created with dismissed state and null dismissal reason', async () => {
      createWrapper({ initialState: dismissed.state, initialDismissalReason: null });

      expect(isSelected(dropdownItemFor(dismissed.state))).toBe(true);

      await mouseOverDismissed();

      reasonDropdownItems().wrappers.forEach((dropdownItem) => {
        expect(isSelected(dropdownItem)).toBe(false);
      });
    });

    it.each(statesWithoutDismissed)(
      `only selects "%s" state when that dropdown item is clicked`,
      async (state) => {
        createWrapper({ initialState: 'some unknown state' });
        const dropdownItem = dropdownItemFor(state);

        await dropdownItem.trigger('click');

        dropdownItems().wrappers.forEach((item) => {
          expect(isSelected(item)).toBe(item.attributes('data-testid') === state);
        });
      },
    );

    it('does not select "dismissed" state when clicking dismissed dropdown item', async () => {
      createWrapper({ initialState: 'some unknown state' });

      await dropdownItemFor(dismissed.state).trigger('click');

      dropdownItems().wrappers.forEach((dropdownItem) => {
        expect(isSelected(dropdownItem)).toBe(false);
      });
    });

    it.each(dismissalReasons)(
      `only selects "%s" reason and "dismissed" state when that reason dropdown item is clicked`,
      async (reason) => {
        createWrapper({ initialState: 'some unknown state' });

        await mouseOverDismissed();
        const reasonDropdownItem = dropdownItemFor(reason);
        await reasonDropdownItem.trigger('click');

        dropdownItems().wrappers.forEach((item) => {
          expect(isSelected(item)).toBe(
            [dismissed.state, reason].includes(item.attributes('data-testid')),
          );
        });
      },
    );

    it.each(statesWithoutDismissed)(
      `does not open dismissal reason dropdown when hovering on %s state`,
      async (state) => {
        createWrapper({ initialState: state });

        await dropdownItemFor(state).trigger('mouseover');

        expect(reasonDropdown().exists()).toBe(false);
      },
    );

    it('disables the status change button if provided in properties', () => {
      createWrapper({
        initialState: dismissed.state,
        initialDismissalReason: dismissalReasons[0],
        disabled: true,
      });

      expect(innerDropdown().props('disabled')).toBe(true);
    });

    it('does not disable the status change button if provided in properties', () => {
      createWrapper({ initialState: dismissed.state, initialDismissalReason: dismissalReasons[0] });

      expect(innerDropdown().props('disabled')).toBe(false);
    });

    it('enables/disables the save button based on if the selected dismissal reason has changed or not', async () => {
      createWrapper({ initialState: dismissed.state, initialDismissalReason: dismissalReasons[0] });

      await mouseOverDismissed();
      const originalItem = selectedReasonItem();

      expect(isDisabled(saveButton())).toBe(true);

      await firstUnselectedReasonItem().trigger('click');

      expect(isDisabled(saveButton())).toBe(false);

      await originalItem.trigger('click');

      expect(isDisabled(saveButton())).toBe(true);
    });
  });

  describe('tests that use the default wrapper', () => {
    beforeEach(() => createWrapper());

    it('enables/disables the save button based on if the selected item has changed or not', async () => {
      const originalItem = selectedItem();

      expect(isDisabled(saveButton())).toBe(true);

      await firstUnselectedItem().trigger('click');

      expect(isDisabled(saveButton())).toBe(false);

      await originalItem.trigger('click');

      expect(isDisabled(saveButton())).toBe(true);
    });

    it('closes the dropdown and fires a change event when clicking the save button', async () => {
      expect(isDisabled(saveButton())).toBe(true);

      await firstUnselectedItem().trigger('click');
      saveButton().vm.$emit('click');
      const changeEvent = wrapper.emitted('change');

      expect(hideDropdownMock).toHaveBeenCalledTimes(1);
      expect(changeEvent).toHaveLength(1);
      expect(changeEvent[0][0]).toEqual(expect.any(Object));
      expect(changeEvent[0][0].payload).toBeUndefined();
    });

    it('includes the reason in payload of event when clicking the save button after selecting dismissed state with a reason', async () => {
      await mouseOverDismissed();
      await dropdownItemFor(dismissalReasons[0]).trigger('click');
      saveButton().vm.$emit('click');
      const changeEvent = wrapper.emitted('change');

      expect(changeEvent[0][0].payload.dismissalReason).toEqual(dismissalReasons[0].toUpperCase());
    });

    it('closes the dropdown without emitting any events when clicking the cancel button', async () => {
      expect(isDisabled(saveButton())).toBe(true);

      await firstUnselectedItem().trigger('click');

      expect(isDisabled(saveButton())).toBe(false);

      cancelButton().vm.$emit('click');

      expect(Object.keys(wrapper.emitted())).toHaveLength(0);
      expect(hideDropdownMock).toHaveBeenCalledTimes(1);
    });

    it('resets the selected item back to the initial item when the dropdown is closed', async () => {
      const initialSelectedItem = selectedItem();

      await firstUnselectedItem().trigger('click');

      expect(selectedItem().element).not.toBe(initialSelectedItem.element);

      innerDropdown().vm.$emit('hide');
      await nextTick();

      expect(selectedItem().element).toBe(initialSelectedItem.element);
    });

    it('updates the selected and initial item when the parent component changes the state and dismissal reason', async () => {
      const stateObject = dismissed;
      const dismissalReason = dismissalReasons[0];

      await wrapper.setProps({
        initialState: stateObject.state,
        initialDismissalReason: dismissalReason,
      });

      expect(innerDropdown().props('text')).toBe(stateObject.buttonText);
      expect(selectedItem().text()).toMatch(DISMISSAL_REASONS[dismissalReason]);
      expect(isDisabled(saveButton())).toBe(true);
    });

    it('opens dismissal reason dropdown when hovering on dismiss state', async () => {
      await mouseOverDismissed();

      expect(reasonDropdown().isVisible()).toBe(true);
    });

    it.each(dismissalReasons)(`sets correct options for %s help popover`, async (reason) => {
      await mouseOverDismissed();

      expect(helpPopoverFor(reason).props().options).toEqual({
        boundary: 'viewport',
        cssClasses: ['gl-pointer-events-none'],
        content: dismissalDescriptions[reason],
        title: DISMISSAL_REASONS[reason],
      });
    });

    it('sets css classes to hide and show help popover', async () => {
      await mouseOverDismissed();

      dismissalReasons.forEach((reason) => {
        const popover = helpPopoverFor(reason);
        expect(popover.classes('gl-visibility-hidden')).toBe(true);
        expect(popover.classes('dismissal-reason-popover')).toBe(true);
      });
    });

    it('destroys popper after hiding reason dropdown', async () => {
      await mouseOverDismissed();
      dropdownItemFor(dismissed.state).trigger('mouseleave');

      expect(wrapper.vm.showDismissalDropdown).toBe(false);
      expect(wrapper.vm.popper).toBe(null);
    });
  });
});
